package org.codefinger.dao.util.magic;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import org.codefinger.dao.asm.BaseTypeInfo;
import org.codefinger.dao.asm.ClassWriter;
import org.codefinger.dao.asm.MethodVisitor;
import org.codefinger.dao.asm.Opcodes;
import org.codefinger.dao.util.AbstractCache.ValueBuilder;
import org.codefinger.dao.util.IdentityCache;
import org.codefinger.dao.util.Lang;

public abstract class MagicField {

	private static final IdentityCache<Field, MagicField>	MAGIC_FIELD_MAP	= new IdentityCache<Field, MagicField>(4096, new MagicFieldBuilder());

	protected MagicField(Field field) {
		this.field = field;
	}

	protected Field	field;

	public Field getField() {
		return field;
	}

	@Override
	public String toString() {
		return field.toString();
	}

	public abstract Object getValue(Object invoker);

	public abstract void setValue(Object invoker, Object value);

	public static MagicField getMagicField(Field field) {
		return getField(field);
	}

	public static MagicField getMagicField(Class<?> clazz, String fieldName) {
		try {
			return getField(clazz.getField(fieldName));
		} catch (NoSuchFieldException e) {
			throw Lang.wrapThrow(e, "Can not find the specified Field.");
		} catch (SecurityException e) {
			throw Lang.wrapThrow(e, "Can not visit the specified Field.");
		}
	}

	public static MagicField getField(Field field) {
		return MAGIC_FIELD_MAP.get(field);
	}

	private static class MagicFieldBuilder implements ValueBuilder<Field, MagicField> {

		@Override
		public MagicField build(Field field) {
			int modifiers = field.getModifiers();

			if (!Modifier.isPublic(modifiers)) {
				throw Lang.makeThrow("The field must be public.");
			}

			ClassWriter cw = new ClassWriter();
			String className = Lang.joinString("PojoXAutocreatedMagicField_", Lang.getLongAdder());
			String superClassName = AsmUtil.getAsmClassName(MagicField.class);

			cw.visit(AsmUtil.CODE_VERSION, Opcodes.ACC_PUBLIC, className, superClassName, null);

			String constructorDesc = AsmUtil.getConstructorDesc(Field.class);
			MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", constructorDesc, null, null);
			mv.visitVarInsn(Opcodes.ALOAD, 0);
			mv.visitVarInsn(Opcodes.ALOAD, 1);
			mv.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassName, "<init>", constructorDesc);
			mv.visitInsn(Opcodes.RETURN);
			mv.visitMaxs(2, 2);
			mv.visitEnd();

			mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getValue", AsmUtil.getMethodDesc(Object.class, Object.class), null, null);

			String fieldClassName = AsmUtil.getAsmClassName(field.getDeclaringClass());
			Class<?> fieldType = field.getType();
			String fieldTypeName = fieldType.getName();
			int stackLength = 1;

			boolean isStatic = Modifier.isStatic(modifiers);

			if (!isStatic) {
				mv.visitVarInsn(Opcodes.ALOAD, 1);
				mv.visitTypeInsn(Opcodes.CHECKCAST, fieldClassName);
				stackLength++;
			}

			String fieldName = field.getName();
			String asmFieldType = AsmUtil.getAsmTypeName(fieldType);

			mv.visitFieldInsn(isStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD, fieldClassName, fieldName, asmFieldType);

			BaseTypeInfo baseTypeInfo = AsmUtil.getBaseTypeInfo(fieldTypeName);

			if (baseTypeInfo != null) {
				mv.visitMethodInsn(Opcodes.INVOKESTATIC, baseTypeInfo.getAsmPackageClassName(), "valueOf", Lang.joinString("(", baseTypeInfo.getAsmBaseClassName(), ")", baseTypeInfo.getAsmPackageClassType()));
			}

			mv.visitInsn(Opcodes.ARETURN);
			mv.visitMaxs(stackLength, 2);
			mv.visitEnd();

			mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "setValue", AsmUtil.getMethodDesc(null, Object.class, Object.class), null, null);

			if (Modifier.isFinal(modifiers)) {
				mv.visitInsn(Opcodes.RETURN);
				mv.visitMaxs(0, 3);
				mv.visitEnd();
			} else {
				stackLength = 1;
				if (!isStatic) {
					mv.visitVarInsn(Opcodes.ALOAD, 1);
					mv.visitTypeInsn(Opcodes.CHECKCAST, fieldClassName);
					stackLength++;
				}
				mv.visitVarInsn(Opcodes.ALOAD, 2);
				String fieldClassTypeName = AsmUtil.getAsmClassName(fieldType);
				mv.visitTypeInsn(Opcodes.CHECKCAST, fieldClassTypeName);

				if (baseTypeInfo != null) {
					mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, fieldClassTypeName, Lang.joinString(fieldTypeName, "Value"), Lang.joinString("()", baseTypeInfo.getAsmBaseClassName()));
					if (fieldType.equals(long.class) || fieldType.equals(double.class)) {
						stackLength++;
					}
				}
				mv.visitFieldInsn(isStatic ? Opcodes.PUTSTATIC : Opcodes.PUTFIELD, fieldClassName, fieldName, asmFieldType);
				mv.visitInsn(Opcodes.RETURN);
				mv.visitMaxs(stackLength, 3);
				mv.visitEnd();
			}

			try {
				return (MagicField) MagicClassLoader.loadByteCodes(className, cw.toByteArray()).getConstructor(Field.class).newInstance(field);
			} catch (Throwable throwable) {
				throw Lang.wrapThrow(throwable, "It's impossable.");
			}
		}

	}

}
